/* License Notice:
**
** This program is free software: you can redistribute it and/or modify
**    it under the terms of the GNU General Public License as published by
**    the Free Software Foundation, either version 3 of the License, or
**    (at your option) any later version.
** This program is distributed in the hope that it will be useful,
**   but WITHOUT ANY WARRANTY; without even the implied warranty of
**   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
**   GNU General Public License for more details.
** You should have received a copy of the GNU General Public License
**   along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

/**
 * @file shader.cpp
 * @author TooOld2Rock'nRoll
 * @see shader.hpp
 */
/*---- Includes ----*/
#include "graphics/shader.hpp"

#include <glm/gtc/type_ptr.hpp>

#include "toolbox/debug.h"


/*---- Enumerations ----*/
/** @brief To differentiate each kind of shader object from each other.  */
enum _shader_type_
{
    VERTEX = 0, ///< [Vertex](https://www.khronos.org/opengl/wiki/Vertex_Shader) shader
    FRAGMENT,   ///< [Fragment](https://www.khronos.org/opengl/wiki/Fragment_Shader) shader
    GEOMETRY,   ///< [Geometry](https://www.khronos.org/opengl/wiki/Geometry_Shader) shader
    COMPUTE,    ///< [Compute](https://www.khronos.org/opengl/wiki/Compute_Shader) shader (not in use!)
    PROGRAM,    ///< shader Program
    _shader_type_count ///< just to keep track of enumeration size
};//end


/*---- Global Variables ----*/
//init static members
std::atomic<ShaderID_t> Shader::last_shader = 0;


/*---- Static Methods ----*/
/**
 * @brief Loads a shader file into memory.
 * @remark if ANY other class needs a "load from file" method, we should migrate a tools.h/.c file
 *
 * @param file - the path to the file.
 * @param buffer - free buffer area to load it's contents.
 *
 * @return The size of the file in bytes if successful.
 *
 * @throws std::invalid_argument - if any of the parameters are null.
 * @throws std::runtime_error - if file can't be opened or is empty.
 */
static size_t _loadShaderFromFile (const char *file, char **buffer)
{
    FILE *fp = nullptr;
    size_t file_size = 0;

    _debug (TRACE, "::%s (shader=%s)", __FUNCTION__, file);

    //check parameters
    if (!file)
        throw std::invalid_argument (fmt::format ("{0}:{1} - file path is NULL!", __LINE__, __FILE__));
    else if (!buffer)
        throw std::invalid_argument (fmt::format ("{0}:{1} - buffer is NULL, can't allocate memory for file!", __LINE__, __FILE__));
    else if (*buffer)
        throw std::invalid_argument (fmt::format ("{0}:{1} - *buffer is not NULL, memory area already being used!", __LINE__, __FILE__));

    //open the file and check it's size
    fp = fopen (file, "r");
    if (!fp)
        throw std::runtime_error (fmt::format ("{0}:{1} - Can't open the file!", __LINE__, __FILE__));

    fseek (fp, 0, SEEK_END);
    file_size = ftell (fp);
    if (file_size == 0)
        throw std::runtime_error (fmt::format ("{0}:{1} - file is empty!", __LINE__, __FILE__));
    rewind (fp);

    //allocate memory and read file
    *buffer = (char *) malloc (file_size + 1);
    fread (*buffer, file_size, 1, fp);
    fclose (fp);

    //set correct end of string
    (*buffer)[file_size] = 0;

    return file_size;
}//End loadShaderFromFile ()


/**
 * @brief Static method that checks for OpenGl errors on the previous operation.
 *
 * @param sid - the shader object identity.
 * @param type - what kind of operation we are looking errors for.
 *
 * @throws std::runtime_error - in case something went wrong wile compiling/linking the shaders and program.
 */
static void _checkCompileErrors (ShaderID_t sid, _shader_type_ type)
{
    int success;

    _debug (TRACE, "::%s (sid=%u, type=%u)", __FUNCTION__, sid, type);

    switch (type)
    {
        case VERTEX:
        case FRAGMENT:
        case GEOMETRY:
            glGetShaderiv (sid, GL_COMPILE_STATUS, &success);
            break;
        //end case VERTEX:

        case PROGRAM:
            glGetProgramiv (sid, GL_LINK_STATUS, &success);
            break;
        //end case PROGRAM:

        default:
            throw std::runtime_error (fmt::format ("{0}:{1} - ERROR: Unknown shader program type {2}!", __LINE__, __FILE__, type));
            return;
    }//end switch (type)

    if (!success)
    {
        char infoLog[1024] = {0,};

        glGetShaderInfoLog (sid, 1024, NULL, infoLog);
        throw std::runtime_error (fmt::format ("{0}:{1} - ERROR::Type: {2}", __LINE__, __FILE__, infoLog));
    }//end if (!success)
}//End checkCompileErrors ()


/*---- Methods Implementation ----*/
/**
 * @brief Shader base Constructor.
 */
Shader::Shader ()
{
    _debug (TRACE, "::%s ()", __FUNCTION__);

    this->ID = glCreateProgram ();
}//End Constructor

/**
 * @brief Shader Destructor.
 */
Shader::~Shader ()
{
    _debug (TRACE, "::%s ()", __FUNCTION__);

    glDeleteProgram (this->ID);
}//End Destructor

/**
 * @brief Sets this shader program to be used by the rendering state.
 *
 * Before executing any operations to a shader, it must first be set to the current rendering state.<br>
 * All methods in this class execute use() when necessary.
 *
 * @see https://www.khronos.org/opengl/wiki/GLAPI/glUseProgram
 */
void Shader::use ()
{
    _debug (TRACE, "::%s ()", __FUNCTION__);

    if (Shader::last_shader == this->ID)
        return; //already using shader

    glUseProgram (this->ID);
    Shader::last_shader = this->ID;
}//End use ()

/**
 * @brief Loads the shader programs into our shader object.
 *
 * Compile the shaders source code, links them to the object shader and checks for errors.
 *
 * @param vertex_src - buffer to the Vertex Shader source code.
 * @param fragment_src - buffer to the Fragment Shader source code.
 * @param geometry_src - optional buffer to the Geometry Shader source code.
 *
 * @throws std::runtime_error - in case something went wrong wile compiling/linking the shaders and program.
 *
 * @see https://www.khronos.org/opengl/wiki/Shader_Compilation
 */
void Shader::compile (const char *vertex_src, const char *fragment_src, const char *geometry_src)
{
    unsigned int sVertex, sFragment, gShader;

    _debug (TRACE, "::%s ()", __FUNCTION__);

    this->_compilation_semaphore.lock ();

    if (this->b_compiled)
    {
        _debug (WARNING, "Shader %u already successfully compiled, aborting...", this->ID);
        return;
    }//end if (this->b_compiled)

    // vertex Shader
    sVertex = glCreateShader (GL_VERTEX_SHADER);
    glShaderSource (sVertex, 1, &vertex_src, NULL);
    glCompileShader (sVertex);
    _checkCompileErrors (sVertex, VERTEX);

    glAttachShader (this->ID, sVertex);

    // fragment Shader
    sFragment = glCreateShader (GL_FRAGMENT_SHADER);
    glShaderSource (sFragment, 1, &fragment_src, NULL);
    glCompileShader (sFragment);
    _checkCompileErrors (sFragment, FRAGMENT);

    glAttachShader (this->ID, sFragment);

    if (geometry_src != nullptr)
    {
        gShader = glCreateShader (GL_GEOMETRY_SHADER);
        glShaderSource (gShader, 1, &geometry_src, NULL);
        glCompileShader (gShader);
        _checkCompileErrors (gShader, GEOMETRY);

        glAttachShader (this->ID, gShader);
    }//end if (geometrySource != nullptr)

    glLinkProgram (this->ID);
    _checkCompileErrors (this->ID, PROGRAM);

    // delete the shaders as they're linked into our program now and no longer necessary
    glDeleteShader (sVertex);
    glDeleteShader (sFragment);
    if (geometry_src != nullptr)
        glDeleteShader (gShader);

    //if we got till here, it means there were no exceptions thrown during the compilation process!
    this->b_compiled = true;
    this->_compilation_semaphore.unlock ();
}//End Compile ()


/**
 * @brief Loads and compiles the necessary shader programs from files.
 *
 * @param vertex_file - path to the vertex shader.
 * @param fragment_file - path to the fragment shader.
 * @param geometry_file - optional, path to the geometry shader.
 *
 * @throws std::invalid_argument - if any of the necessary parameters are null.
 */
void Shader::loadFromFile (const char *vertex_file, const char *fragment_file, const char *geometry_file)
{
    char *vert = nullptr,
         *frag = nullptr,
         *geo = nullptr;

    //check parameters
    if (!vertex_file)
        throw std::invalid_argument (fmt::format ("{0}:{1} - vertex path is NULL!", __LINE__, __FILE__));
    else if (!fragment_file)
        throw std::invalid_argument (fmt::format ("{0}:{1} - fragment path is NULL!", __LINE__, __FILE__));

    _debug (TRACE, "::%s (vertex=%s, fragment=%s, geometry=%s)", __FUNCTION__, vertex_file, fragment_file, (geometry_file ? geometry_file : "null"));

    _loadShaderFromFile (vertex_file, &vert);
    _loadShaderFromFile (fragment_file, &frag);
    if (geometry_file)
        _loadShaderFromFile (geometry_file, &geo);

    this->compile (vert, frag, geo);

    free (vert);
    free (frag);
    if (geo)
        free (geo);
}//End loadFromFile ()


/**
 * @brief Sets a float value to a shader uniform.
 *
 * @param name - the uniform name.
 * @param value - value to set.
 */
void Shader::setUniform (const char *name, float value)
{
    _debug (TRACE, "::%s (name='%s', value=%.2f)", __FUNCTION__, name, value);

    this->use (); //not using shader yet

    glUniform1f (glGetUniformLocation(this->ID, name), value);
}

/**
 * @brief Sets a int value to a shader uniform.
 *
 * @param name - the uniform name.
 * @param value - value to set.
 */
void Shader::setUniform (const char *name, int value)
{
    _debug (TRACE, "::%s (name='%s', value=%d)", __FUNCTION__, name, value);

    this->use (); //not using shader yet

    glUniform1i (glGetUniformLocation(this->ID, name), value);
}

/**
 * @brief Sets a glm::vec2 to a shader uniform.
 *
 * @param name - the uniform name.
 * @param x - X component of the vector.
 * @param y - Y component of the vector.
 */
void Shader::setUniform (const char *name, float x, float y)
{
    _debug (TRACE, "::%s (name='%s', x=%.2f, y=%.2f)", __FUNCTION__, name, x, y);

    this->use (); //not using shader yet

    glUniform2f (glGetUniformLocation(this->ID, name), x, y);
}

/**
 * @brief Sets a glm::vec3 to a shader uniform.
 *
 * @param name - the uniform name.
 * @param x - X component of the vector.
 * @param y - Y component of the vector.
 * @param z - Z component of the vector.
 */
void Shader::setUniform (const char *name, float x, float y, float z)
{
    _debug (TRACE, "::%s (name='%s', x=%.2f, y=%.2f, z=%.2f)", __FUNCTION__, name, x, y, z);

    this->use (); //not using shader yet

    glUniform3f (glGetUniformLocation(this->ID, name), x, y, z);
}

/**
 * @brief Sets a glm::vec4 to a shader uniform.
 *
 * @param name - the uniform name.
 * @param x - X component of the vector.
 * @param y - Y component of the vector.
 * @param z - Z component of the vector.
 * @param w - W component of the vector.
 */
void Shader::setUniform (const char *name, float x, float y, float z, float w)
{
    _debug (TRACE, "::%s (name='%s', x=%.2f, y=%.2f, z=%.2f, w=%.2f)", __FUNCTION__, name, x, y, z, w);

    this->use (); //not using shader yet

    glUniform4f (glGetUniformLocation(this->ID, name), x, y, z, w);
}

/**
 * @brief Sets a glm::mat4 to a shader uniform.
 *
 * @param name - the uniform name.
 * @param matrix - reference to the matrix to be set.
 */
void Shader::setUniform (const char *name, const glm::mat4 &matrix)
{
    _debug (TRACE, "::%s (name='%s')", __FUNCTION__, name);

    this->use (); //not using shader yet

    glUniformMatrix4fv (glGetUniformLocation(this->ID, name), 1, false, glm::value_ptr(matrix));
}


/**
 * @brief Sets a Orthographic Projection for the shader.
 * @remark Better suited for 2D aplications.
 *
 * @param left - value for min X coordinate at left of the Viewport, probably 0.
 * @param top - value for max Y coordinate at bottom of Viewport, probably 0.
 * @param right - value for max X coordinate at right of the Viewport, probably Viewport width.
 * @param bottom - value for min Y coordinate at bottom of Viewport, probably Viewport height.
 * @param zNear - min Z coordinate of a object to print.
 * @param zFar - max Z coordinate of a object to print.
 * @param uniform_name - the name of the uniform in the shader that represents the projection.
 *
 * @return The projection matrix.
 */
glm::mat4 Shader::setOrthographicProjection (float left, float top, float right, float bottom, float zNear, float zFar, const char *uniform_name)
{
    glm::mat4 proj_mat = glm::mat4 (1.f);

    _debug (TRACE, "::%s (left=%.2f, top=%.2f, right=%.2f, bottom=%.2f, zNear=%.2f, zFar=%.2f, name='%s')", __FUNCTION__, left, top, right, bottom, zNear, zFar, uniform_name);

    proj_mat = glm::ortho (left, right, bottom, top, zNear, zFar);
    this->setUniform (uniform_name, proj_mat);

    return proj_mat;
}//End setOrthographicProjection ()

/**
 * @brief Sets a Perspective Projection for the shader.
 * @remark Better suited for 3D aplications.
 *
 * @param fovy - Field Of View in Y direction in radians, try 45° to start and see what happens.
 * @param aspect - Aspect ratio for the field of view in X, basically Viewport width divided by Viewport height.
 * @param zNear - how close to the viewer to clip Z? Always positive.
 * @param zFar - how far from the viewer to clip Z? Always positive.
 * @param uniform_name - the name of the uniform in the shader that represents the projection.
 *
 * @return The projection matrix.
 */
glm::mat4 Shader::setPerspectiveProjection (float fovy, float aspect, float zNear, float zFar, const char *uniform_name)
{
    glm::mat4 proj_mat = glm::mat4 (1.f);

    _debug (TRACE, "::%s (fovy=%.2f, aspect=%.2f, zNear=%.2f, zFar=%.2f, name='%s')", __FUNCTION__, fovy, aspect, zNear, zFar, uniform_name);

    proj_mat = glm::perspective (fovy, aspect, zNear, zFar);
    this->setUniform (uniform_name, proj_mat);

    return proj_mat;
}//end setPerspectiveProjection ()

/**
 * @brief Sets the Camera View uniform.
 *
 * @param view - the camera view matrix.
 * @param uniform_name - the camera view uniform name in the shader.
 */
void Shader::setCameraView (glm::mat4 view, const char *uniform_name)
{
    _debug (TRACE, "::%s (name='%s')", __FUNCTION__, uniform_name);

    this->setUniform (uniform_name, view);
}//End setCameraView ()

